Learn in 10 minutes

Learn in 10 minutes

Impara Python in 10 minuti

Python è un linguaggio di programmazione di alto livello e interpretato, noto per la sua sintassi concisa e le sue potenti funzionalità. Questo tutorial si basa sull’ultima versione di Python 3.13+, aiutandoti a imparare Python rapidamente.

1. Scrivere il tuo primo programma Python

Iniziamo con un programma semplice. Crea un file chiamato hello.py e inserisci il seguente codice:

print("Ciao, Mondo!")

Salva il file ed esegui il seguente comando nel terminale o nella riga di comando:

python hello.py

L’output sarà:

Ciao, Mondo!

Questo semplice programma dimostra la funzionalità di output di base di Python. La funzione print() viene utilizzata per visualizzare informazioni testuali nella console.

2. Sintassi di base

La sintassi di Python è semplice e facile da comprendere. Python utilizza l’indentazione per definire i blocchi di codice, a differenza di altri linguaggi che usano parentesi graffe {}.

# Questo è un commento
print("Ciao, Mondo!")

Regole di sintassi di base in Python:

  • Indentazione: Per impostazione predefinita, si usano 4 spazi per indicare il livello di un blocco di codice. Ad esempio, il codice all’interno di funzioni o cicli deve essere indentato.
  • Commenti: I commenti su una singola riga iniziano con #, mentre i commenti su più righe usano virgolette triple """ o '''.
  • Istruzioni: Di solito una istruzione per riga, non è necessario il punto e virgola ; alla fine.
  • Blocchi di codice: Definiti dall’indentazione, come in if, for o nei corpi delle funzioni.

Esempio con commenti su più righe:

"""
Questo è un commento su più righe,
che si estende su diverse righe.
"""

L’indentazione è una caratteristica chiave della sintassi di Python, utilizzata per definire la struttura gerarchica dei blocchi di codice:

if True:
    print("Questa riga è indentata")
    print("Anche questa riga è indentata")
print("Questa riga non è indentata")

3. Variabili e tipi di dati

In Python, le variabili sono contenitori per memorizzare dati. Python è un linguaggio a tipizzazione dinamica, il che significa che non è necessario dichiarare in anticipo il tipo di una variabile.

Regole di base per la denominazione delle variabili:

  • I nomi delle variabili possono contenere solo lettere, numeri e trattini bassi.
  • I nomi delle variabili non possono iniziare con un numero.
  • I nomi delle variabili sono sensibili alle maiuscole.
  • Le parole chiave di Python non possono essere usate come nomi di variabili.

Il tipo di variabile è determinato dal valore assegnato. I principali tipi di dati di base di Python sono:

  • Intero (int): ad esempio, 42 o -10, senza limite di dimensione.
  • Float (float): ad esempio, 3.14 o 2.5e3 (notazione scientifica).
  • Stringa (str): ad esempio, "ciao" o 'mondo', usando virgolette singole o doppie.
  • Booleano (bool): True o False.
  • NoneType (None): Rappresentato da None, indica un valore nullo o assente.

Python supporta gli hint di tipo per migliorare la leggibilità del codice, utilizzati per il controllo statico e il supporto IDE, senza influenzare il comportamento in esecuzione:

name: str = "Alice"
age: int = 25

3.1 Tipi numerici (Number)

Python supporta tre tipi numerici: intero (int), float (float) e complesso (complex).

# Intero
age = 25
population = 1000000

# Float
temperature = 36.5
pi = 3.14159

# Complesso
complex_num = 3 + 4j

3.2 Stringa (String)

Le stringhe sono sequenze di caratteri, racchiuse tra virgolette singole o doppie.

single_quote = 'Stringa con virgolette singole'
double_quote = "Stringa con virgolette doppie"
multiline = """Questa è una
stringa su più righe"""

Operazioni sulle stringhe:

text = "Programmazione Python"
print(len(text))        # Lunghezza della stringa
print(text.upper())     # Converti in maiuscolo
print(text.lower())     # Converti in minuscolo
print(text[0])          # Accedi al primo carattere
print(text[2:6])        # Slicing della stringa

3.3 Tipo booleano (Boolean)

Il tipo booleano ha due valori: True e False.

is_active = True
is_complete = False

# Operazioni booleane
result1 = True and False  # False
result2 = True or False   # True
result3 = not True        # False

3.4 Tipo None (None)

None rappresenta uno stato nullo o senza valore.

value = None

if value is None:
    print("Il valore è nullo")

4. Strutture dati

Python fornisce diverse strutture dati integrate per memorizzare e manipolare i dati. Di seguito sono elencate le strutture dati più comuni e il loro utilizzo.

4.1 Lista (List)

Una lista è una collezione ordinata e modificabile. Puoi aggiungere, rimuovere o modificare elementi in una lista. Le liste sono definite usando parentesi quadre [].

numbers = [1, 2, 3, 4, 5]
numbers.append(6)      # Aggiungi elemento
numbers.insert(0, 0)   # Inserisci in una posizione specifica
numbers.remove(3)      # Rimuovi un valore specifico
numbers[0] = 10        # Modifica elemento
print(numbers)         # [10, 2, 4, 5, 6]

Slicing delle liste per accedere a sottoliste:

numbers = [10, 20, 30, 40, 50]
print(numbers[1:4])    # Output: [20, 30, 40]
print(numbers[:3])     # Output: [10, 20, 30]
print(numbers[-2:])    # Output: [40, 50]

Comprensione delle liste:

squares = [x**2 for x in range(5)]
print(squares)         # [0, 1, 4, 9, 16]

even Ascoltare even_squares = [x**2 for x in range(10) if x % 2 == 0]
print(even_squares)    # [0, 4, 16, 36, 64]

4.2 Tupla (Tuple)

Una tupla è una collezione ordinata ma immutabile. Una volta creata, i suoi elementi non possono essere modificati. Le tuple sono definite usando parentesi tonde (). Grazie alla loro immutabilità, le tuple sono generalmente più veloci delle liste e possono essere usate come chiavi di dizionari.

point = (10, 20)
x, y = point  # Unpacking
print(x, y)   # Output: 10 20

Tuple con un solo elemento richiedono una virgola:

single_tuple = (42,)

4.3 Dizionario (Dict)

Un dizionario è una collezione non ordinata (ordinata in Python 3.7+) di coppie chiave-valore. Ogni chiave è unica ed è associata a un valore. I dizionari sono definiti usando parentesi graffe {}.

student = {
    "name": "John",
    "age": 20,
    "major": "Computer Science"
}

# Accesso e modifica del dizionario
print(student["name"])
student["age"] = 21
student["gpa"] = 3.8

# Accesso sicuro
print(student.get("phone", "Non fornito"))

# Iterazione sul dizionario
for key, value in student.items():
    print(f"{key}: {value}")

Comprensione dei dizionari:

# Crea un dizionario dei quadrati
squares_dict = {x: x**2 for x in range(5)}
print(squares_dict)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Comprensione condizionale dei dizionari
even_squares_dict = {x: x**2 for x in range(10) if x % 2 == 0}
print(even_squares_dict)  # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

4.4 Insieme (Set)

Un insieme è una collezione non ordinata senza elementi duplicati. Definito usando parentesi graffe {} o set().

# Creazione di un insieme
fruits = {"apple", "banana", "orange"}
numbers = set([1, 2, 3, 3, 4, 4, 5])  # Deduplicazione automatica
print(numbers)  # {1, 2, 3, 4, 5}

# Operazioni sugli insiemi
set1 = {1, 2, 3, 4}
set2 = {3, 4, 5, 6}

print(set1 | set2)  # Unione: {1, 2, 3, 4, 5, 6}
print(set1 & set2)  # Intersezione: {3, 4}
print(set1 - set2)  # Differenza: {1, 2}
print(set1 ^ set2)  # Differenza simmetrica: {1, 2, 5, 6}

5. Operazioni e Operatori

Python fornisce un ricco set di operatori per vari calcoli e confronti, inclusi operatori aritmetici, di confronto, logici, bitwise e di identità.

  • Operatori aritmetici: +, -, *, /, // (divisione intera), % (modulo), ** (esponenziazione).
  • Operatori di confronto: ==, !=, >, <, >=, <=.
  • Operatori logici: and, or, not.
  • Operatori di appartenenza: in, not in.
  • Operatori di identità: is, is not.

5.1 Operatori aritmetici

Gli operatori aritmetici sono usati per operazioni matematiche. La precedenza degli operatori segue le regole matematiche (ad esempio, ** ha una precedenza maggiore rispetto a + o -). Le parentesi () possono essere usate per cambiare la precedenza.

a, b = 10, 3

print(f"Somma: {a + b}")      # 13
print(f"Sottrazione: {a - b}")   # 7
print(f"Moltiplicazione: {a * b}")  # 30
print(f"Divisione: {a / b}")      # 3.333...
print(f"Divisione intera: {a // b}")  # 3
print(f"Modulo: {a % b}")      # 1
print(f"Esponenziazione: {a ** b}")  # 1000

5.2 Operatori di confronto

Gli operatori di confronto confrontano due valori e restituiscono un booleano (True o False).

x, y = 5, 10

print(f"Uguale: {x == y}")     # False
print(f"Non uguale: {x != y}") # True
print(f"Maggiore di: {x > y}")  # False
print(f"Minore di: {x < y}")  # True
print(f"Maggiore o uguale: {x >= y}")  # False
print(f"Minore o uguale: {x <= y}")  # True

5.3 Operatori logici

Gli operatori logici combinano o manipolano valori booleani (True o False), tipicamente usati nelle istruzioni condizionali.

a, b = True, False

print(f"Operazione AND: {a and b}")  # False
print(f"Operazione OR: {a or b}")    # True
print(f"Operazione NOT: {not a}")    # False

5.4 Operatori di identità

L’operatore is confronta l’identità di due oggetti, verificando se fanno riferimento allo stesso indirizzo di memoria (non solo valori uguali). Differisce da ==, che confronta il contenuto.

list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1

print(f"list1 is list2: {list1 is list2}")    # False
print(f"list1 is list3: {list1 is list3}")    # True
print(f"list1 == list2: {list1 == list2}")    # True

5.5 Operatori di appartenenza

Gli operatori di appartenenza verificano se un valore è membro di una sequenza (ad esempio, lista, tupla, stringa, insieme), restituendo un booleano.

fruits = ["apple", "banana", "orange"]

print(f"'apple' in fruits: {'apple' in fruits}")        # True
print(f"'grape' not in fruits: {'grape' not in fruits}")  # True

6. Controllo del flusso

Python fornisce diverse istruzioni di controllo del flusso per gestire l’ordine di esecuzione di un programma.

6.1 Istruzioni if

L’istruzione if valuta una condizione ed esegue il suo blocco se la condizione è True. Usa elif e else per condizioni complesse.

age = 20
if age >= 18:
    print("Adulto")
elif age >= 13:
    print("Adolescente")
else:
    print("Bambino")

6.2 Cicli for

Il ciclo for itera su un oggetto iterabile (ad esempio, lista, tupla, stringa o range).

# Iterazione su una lista
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

# Utilizzo di range() per il ciclo
for i in range(5):
    print(i)  # Output: 0, 1, 2, 3, 4

6.3 Cicli while

Il ciclo while continua a eseguire un blocco fintanto che la sua condizione rimane True.

count = 0
while count < 5:
    print(count)
    count += 1
  • break e continue: break esce dal ciclo, continue salta l’iterazione corrente.
for i in range(10):
    if i == 5:
        break
    if i % 2 == 0:
        continue
    print(i)  # Output: 1, 3

6.4 Istruzioni match

Introdotte in Python 3.10, le istruzioni match forniscono un potente pattern matching strutturale, agendo come alternativa avanzata alle catene if/elif/else.

http_status = 200

match http_status:
    case 200 | 201:
        print("Successo")
    case 404:
        print("Non trovato")
    case 500:
        print("Errore del server")
    case _:  # Wildcard, corrisponde a qualsiasi altro caso
        print("Stato sconosciuto")

7. Input e Output

7.1 Input e Output di base

Usa la funzione input() per ottenere input dall’utente e la funzione print() per visualizzare informazioni.

# Ottenere input dall'utente
name = input("Inserisci il tuo nome: ")
age = int(input("Inserisci la tua età: "))

# Visualizzare informazioni
print("Benvenuto", name)
print("Hai", age, "anni")

7.2 Output formattato (f-strings)

Introdotte in Python 3.6+, le f-strings (stringhe letterali formattate) sono un modo comodo e potente per formattare le stringhe. Prefissa una stringa con f o F e incorpora variabili o espressioni tra parentesi graffe {}.

user = "Alice"
items = 3
total_cost = 45.5

# Utilizzo di f-string per un messaggio formattato
message = f"L'utente {user} ha acquistato {items} articoli per ${total_cost:.2f}."
print(message)

# Espressioni nelle f-strings
print(f"2 + 3 equivale a {2 + 3}")

Espressioni e chiamate di funzione nelle f-strings:

width = 10
height = 5

# Utilizzo di espressioni nelle f-strings
area = f"Area del rettangolo: {width * height}"
print(area)

# Chiamata di funzioni
text = "python"
formatted = f"Maiuscolo: {text.upper()}, Lunghezza: {len(text)}"
print(formatted)

Opzioni di formattazione nelle f-strings:

pi = 3.14159265359
large_number = 1234567

# Formattazione dei numeri
print(f"Pi (2 decimali): {pi:.2f}")
print(f"Pi (4 decimali): {pi:.4f}")
print(f"Numero grande (migliaia): {large_number:,}")
print(f"Percentuale: {0.85:.1%}")

# Allineamento delle stringhe
name = "Python"
print(f"Allineamento a sinistra: '{name:<10}'")
print(f"Allineamento a destra: '{name:>10}'")
print(f"Allineamento al centro: '{name:^10}'")

Formattazione di data e ora con f-strings:

from datetime import datetime

now = datetime.now()
print(f"Ora corrente: {now}")
print(f"Ora formattata: {now:%Y-%m-%d %H:%M:%S}")
print(f"Solo data: {now:%Y-%m-%d}")

9. Funzioni

Le funzioni in Python sono blocchi di codice riutilizzabili per attività specifiche, definite usando la parola chiave def, che supportano parametri predefiniti, argomenti variabili e argomenti con nome.

Definizione di base di una funzione:

def greet(name):
    """Funzione di saluto"""
    return f"Ciao, {name}!"

# Chiamata della funzione
message = greet("John")
print(message)

Argomenti con nome:

Gli argomenti con nome vengono passati usando la sintassi nome_parametro=valore.

def greet(name, greeting="Ciao"):
    return f"{greeting}, {name}!"

print(greet("Alice"))          # Output: Ciao, Alice!
print(greet("Bob", "Salve"))     # Output: Salve, Bob!

Argomenti variabili:

Gli argomenti variabili consentono alle funzioni di accettare un numero arbitrario di argomenti, sia posizionali (*args) che con nome (**kwargs).

# Argomenti posizionali variabili (*args)
def sum_numbers(*args):
    return sum(args)

print(sum_numbers(1, 2, 3, 4))  # Output: 10

# Argomenti con nome variabili (**kwargs)
def print_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_kwargs(name="Alice", age=25, city="Pechino")

Annotazioni di funzione:

Le annotazioni di funzione aggiungono metadati descrittivi ai parametri e ai valori di ritorno delle funzioni, migliorando leggibilità e documentazione.

def calculate_area(length: float, width: float) -> float:
    """Calcola l'area del rettangolo"""
    return length * width

def process_user(name: str, age: int, active: bool = True) -> dict:
    """Elabora le informazioni dell'utente"""
    return {
        "name": name,
        "age": age,
        "active": active
    }

10. Espressioni Lambda

Le espressioni lambda creano funzioni anonime, fornendo un modo conciso per definire piccole funzioni.

square = lambda x: x ** 2
print(square(5))  # Output: 25

Comunemente usate con funzioni di ordine superiore come map o filter:

numbers = [1, 2, 3, 4]
squares = list(map(lambda x: x ** 2, numbers))
print(squares)  # Output: [1, 4, 9, 16]

11. Classi e Oggetti

Python supporta la programmazione orientata agli oggetti usando la parola chiave class:

class Person:
    """Classe Persona"""
    
    def __init__(self, name, age):
        """Costruttore"""
        self.name = name
        self.age = age
    
    def introduce(self):
        """Metodo di introduzione"""
        return f"Sono {self.name}, ho {self.age} anni"
    
    def have_birthday(self):
        """Metodo del compleanno"""
        self.age += 1
        return f"{self.name} ha compiuto gli anni, ora ha {self.age} anni"

# Creazione di oggetti
person1 = Person("John", 25)
person2 = Person("Jane", 30)

print(person1.introduce())
print(person2.have_birthday())

11.1 Attributi di classe e di istanza

In Python, gli attributi di classe e di istanza sono due tipi di attributi per memorizzare dati nelle classi e negli oggetti.

  • Attributi di classe: Definiti fuori dai metodi, appartengono alla classe stessa e sono condivisi tra tutte le istanze.
  • Attributi di istanza: Definiti nei metodi (di solito __init__), legati a istanze specifiche tramite self.
class Student:
    # Attributi di classe
    school = "Stanford University"
    student_count = 0
    
    def __init__(self, name, major):
        # Attributi di istanza
        self.name = name
        self.major = major
        Student.student_count += 1
    
    @classmethod
    def get_student_count(cls):
        """Metodo di classe"""
        return cls.student_count
    
    @staticmethod
    def is_valid_age(age):
        """Metodo statico"""
        return 0 < age < 150

# Esempio di utilizzo
student1 = Student("John", "Computer Science")
student2 = Student("Jane", "Matematica")

print(f"Scuola: {Student.school}")
print(f"Totale studenti: {Student.get_student_count()}")
print(f"Età valida: {Student.is_valid_age(20)}")

11.2 Ereditarietà di classe

L’ereditarietà consente a una classe (sottoclasse) di ereditare attributi e metodi da un’altra classe (classe genitore), permettendo il riutilizzo e l’estensione del codice.

class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    def make_sound(self):
        return f"{self.name} emette un suono"
    
    def info(self):
        return f"{self.name} è un {self.species}"

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Dog")
        self.breed = breed
    
    def make_sound(self):
        return f"{self.name} abbaia"
    
    def fetch(self):
        return f"{self.name} riporta la palla"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, "Cat")
        self.color = color
    
    def make_sound(self):
        return f"{self.name} miagola"
    
    def climb(self):
        return f"{self.name} si arrampica su un albero"

# Utilizzo dell'ereditarietà
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Mimi", "Arancione")

print(dog.info())
print(dog.make_sound())
print(dog.fetch())

print(cat.info())
print(cat.make_sound())
print(cat.climb())

11.3 Metodi speciali (Metodi magici)

I metodi speciali (o metodi magici) sono metodi con doppio underscore che definiscono comportamenti specifici, chiamati automaticamente da Python in determinati scenari.

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def __str__(self):
        """Rappresentazione come stringa"""
        return f"Rettangolo({self.width}x{self.height})"
    
    def __repr__(self):
        """Rappresentazione ufficiale come stringa"""
        return f"Rettangolo(width={self.width}, height={self.height})"
    
    def __eq__(self, other):
        """Confronto di uguaglianza"""
        if isinstance(other, Rectangle):
            return self.width == other.width and self.height == other.height
        return False
    
    def __lt__(self, other):
        """Confronto minore di (per area)"""
        if isinstance(other, Rectangle):
            return self.area() < other.area()
        return NotImplemented
    
    def __add__(self, other):
        """Operazione di somma"""
        if isinstance(other, Rectangle):
            return Rectangle(self.width + other.width, self.height + other.height)
        return NotImplemented
    
    def area(self):
        """Calcola l'area"""
        return self.width * self.height

# Utilizzo dei metodi speciali
rect1 = Rectangle(3, 4)
rect2 = Rectangle(5, 6)
rect3 = Rectangle(3, 4)

print(rect1)           # Rettangolo(3x4)
print(repr(rect1))     # Rettangolo(width=3, height=4)
print(rect1 == rect3)  # True
print(rect1 < rect2)   # True
print(rect1 + rect2)   # Rettangolo(8x10)

12. Gestori di contesto

I gestori di contesto garantiscono l’acquisizione e il rilascio corretto delle risorse, comunemente usati con l’istruzione with per la gestione delle risorse, come file o connessioni a database.

12.1 Utilizzo dei gestori di contesto

Le operazioni sui file sono un caso d’uso comune per i gestori di contesto:

with open("example.txt", "r") as file:
    content = file.read()
    print(content)

L’istruzione with garantisce che il file venga chiuso automaticamente dopo le operazioni.

12.2 Gestori di contesto personalizzati

Crea gestori di contesto personalizzati definendo i metodi __enter__ e __exit__:

class DatabaseConnection:
    def __init__(self, database_name):
        self.database_name = database_name
        self.connection = None
    
    def __enter__(self):
        """Chiamato all'ingresso nel contesto"""
        print(f"Connessione al database: {self.database_name}")
        self.connection = f"Connessione a {self.database_name}"
        return self.connection
    
    def __exit__(self, exc_type, exc_value, traceback):
        """Chiamato all'uscita dal contesto"""
        print(f"Chiusura della connessione al database: {self.database_name}")
        if exc_type:
            print(f"Si è verificata un'eccezione: {exc_type.__name__}: {exc_value}")
        self.connection = None
        return False  # Non sopprime le eccezioni

# Utilizzo del gestore di contesto personalizzato
with DatabaseConnection("user_database") as conn:
    print(f"Uso della connessione: {conn}")
    print("Esecuzione delle operazioni sul database...")

Utilizzo del modulo contextlib per creare gestori di contesto:

from contextlib import contextmanager
import time

@contextmanager
def timer(operation_name):
    """Gestore di contesto per il timer"""
    print(f"Inizio {operation_name}")
    start_time = time.time()
    try:
        yield
    finally:
        end_time = time.time()
        print(f"{operation_name} completato in {end_time - start_time:.2f} secondi")

# Utilizzo del gestore di contesto basato su decoratore
with timer("elaborazione dati"):
    # Simulazione di un'operazione lunga
    time.sleep(1)
    print("Elaborazione dati...")

13. Gestione delle eccezioni

La gestione delle eccezioni garantisce la robustezza del programma, usando try, except, else e finally per gestire le eccezioni.

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Impossibile dividere per zero!")
else:
    print("Divisione riuscita")
finally:
    print("Questo viene sempre eseguito")

Output:

Impossibile dividere per zero!
Questo viene sempre eseguito

14. Operazioni sui file

Python fornisce metodi semplici per leggere e scrivere file, tipicamente usati con i gestori di contesto.

14.1 Lettura dei file

Lettura del contenuto di un file di testo:

with open("example.txt", "r") as file:
    content = file.read()
    print(content)

Lettura riga per riga:

with open("example.txt", "r") as file:
    for line in file:
        print(line.strip())

14.2 Scrittura sui file

Scrittura su un file di testo:

with open("output.txt", "w") as file:
    file.write("Ciao, Python!\n")

Aggiunta di contenuto:

with open("output.txt", "a") as file:
    file.write("Aggiunta di nuovo contenuto.\n")

15. Moduli e Pacchetti

I moduli sono file contenenti codice Python, e i pacchetti sono directory contenenti più moduli. Importa i moduli usando import:

import math
print(math.sqrt(16))  # Output: 4.0

Esempio di modulo personalizzato (supponiamo che il file sia chiamato mymodule.py):

# mymodule.py
def say_hello():
    return "Ciao dal modulo!"

Importazione e utilizzo:

import mymodule
print(mymodule.say_hello())  # Output: Ciao dal modulo!

16. Ambito e Namespace

16.1 Ambito

L’ambito definisce la regione in cui una variabile è accessibile. Python segue la regola LEGB per la ricerca delle variabili:

  • L (Locale): All’interno di una funzione o metodo di classe.
  • E (Enclosing): Nella funzione esterna di una funzione annidata (closure).
  • G (Globale): A livello di modulo.
  • B (Built-in): Funzioni ed eccezioni incorporate come print() o len().
x = "x globale"

def outer_func():
    x = "x enclosing"
    def inner_func():
        x = "x locale"
        print(x) # Accede all'ambito locale x
    inner_func()
    print(x) # Accede all'ambito enclosing x

outer_func()
print(x) # Accede all'ambito globale x

Utilizzo di global o nonlocal per modificare le variabili dell’ambito:

x = "globale"
def modify_global():
    global x
    x = "modificato"
modify_global()
print(x)  # Output: modificato

16.2 Namespace

Un namespace è una mappatura da nomi a oggetti, come un dizionario in cui le chiavi sono i nomi delle variabili e i valori sono gli oggetti.

Ogni modulo, funzione e classe ha il proprio namespace, prevenendo conflitti di nomi. Usa globals() e locals() per ispezionare i namespace.

a_variable = 10

def some_function():
    b_variable = 20
    # locals() restituisce il dizionario del namespace locale corrente
    print(f"Locali: {locals()}")

print(f"Globali: {globals().keys()}") # Stampa le chiavi del namespace globale
some_function()

17. Generatori

I generatori sono iteratori speciali che generano valori su richiesta, invece di creare tutti i valori contemporaneamente, rendendoli efficienti in termini di memoria per grandi set di dati.

I generatori usano la parola chiave yield per restituire valori in modo pigro:

def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

for num in fibonacci(5):
    print(num)  # Output: 0, 1, 1, 2, 3

18. Multithreading

Il multithreading consente a un programma di eseguire più operazioni contemporaneamente, utile per attività legate all’I/O come richieste di rete o operazioni sui file.

Il modulo threading di Python fornisce strumenti per creare e gestire thread. A causa del Global Interpreter Lock (GIL), il multithreading non raggiunge un vero parallelismo CPU in un singolo processo, ma migliora significativamente le prestazioni per attività legate all’I/O.

import threading
import time

def worker(thread_name):
    print(f"Thread {thread_name} in avvio...")
    time.sleep(2) # Simula un'operazione lunga
    print(f"Thread {thread_name} terminato.")

# Crea i thread
thread1 = threading.Thread(target=worker, args=("A",))
thread2 = threading.Thread(target=worker, args=("B",))

# Avvia i thread
thread1.start()
thread2.start()

# Aspetta che tutti i thread completino
thread1.join()
thread2.join()

print("Tutti i thread completati.")

19. Programmazione asincrona

La programmazione asincrona è ideale per scenari ad alto I/O e alta concorrenza, usando un ciclo di eventi per gestire le attività invece dei thread.

Python supporta la programmazione asincrona tramite la libreria asyncio e la sintassi async/await.

  • async def: Definisce una coroutine.
  • await: Mette in pausa l’esecuzione della coroutine, aspettando che un oggetto awaitable completi.
import asyncio

async def say_hello():
    print("Ciao")
    await asyncio.sleep(1) # Sleep non bloccante, simula I/O
    print("Mondo")

async def main():
    # Crea e aspetta un'attività
    await asyncio.create_task(say_hello())

# Esegue la coroutine principale
asyncio.run(main())

Output:

Ciao
Mondo